import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.glfw.Callbacks;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFWKeyCallback;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengles.GLES;
import org.lwjgl.opengles.GLES30;
import org.lwjgl.system.Configuration;
import org.lwjgl.system.FunctionProvider;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.ThreadLocalUtil;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengles.GLES20.*;

/**
 * OpenGL ES 3.0 的 Demo
 * <li>glfw的基本使用;
 * <li>在桌面平台使用ES版本OpenGL的方法;
 * <li>基本的Vertex Shader和Fragment Shader;
 * <li>VBO, EBO, VAO的基本用法;
 * <li>与native库交换数据所用的高效的MemoryStack;
 * <li>完整的初始化, 循环, 销毁三大步骤;
 * <li>除VAO外与 ES 2.0 版本兼容;
 * <li>稍微修改几处代码可用于 OpenGL 3.0 版本;
 * <li>同时依赖opengl的原因是这里的桌面平台opengles的底层实现转义成了对opengl的调用.
 * 也可以通过google的ANGLE库(libGLESv2.dll,libEGL.dll)来支持, 不过会麻烦些.
 *
 * @see <a href="https://learnopengl.com/Getting-started/Hello-Triangle">Hello Triangle</a>
 * @see <a href="https://www.lwjgl.org/guide">LWJGL 3 Guide</a>
 * @see <a href="https://stackoverflow.com/questions/43537585/there-is-no-opengl-es-context-current-in-the-current-thread-lwjgl-using-opengl">LWJGL using OpenGL ES 3.0</a>
 */
@SuppressWarnings("EmptyTryBlock")
public final class GlesDemo {
	private static long window;
	private static int program;
	private static int mvpLoc, colorLoc, tex0Loc, timeLoc;
	private static int tex0, vao;

	private static void init() {
		try (final GLFWErrorCallback _ = glfwSetErrorCallback(GLFWErrorCallback.createPrint(System.err))) {
		}
		if (!glfwInit())
			throw new AssertionError("glfwInit failed");
		glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
		glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
		glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
		if ((window = glfwCreateWindow(512, 512, "LwjglDemo", 0, 0)) == 0)
			throw new AssertionError("glfwCreateWindow failed");
		try (final GLFWKeyCallback _ = glfwSetKeyCallback(window, (win, key, _, action, _) -> {
			if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE)
				glfwSetWindowShouldClose(win, true);
		})) {
		}
		glfwMakeContextCurrent(window);
		glfwSwapInterval(1);

		Configuration.OPENGLES_EXPLICIT_INIT.set(true);
		FunctionProvider fp = GL.getFunctionProvider();
		ThreadLocalUtil.setFunctionMissingAddresses(0); // prevent 'setFunctionMissingAddresses has been called already'
		GLES.create(fp);
		GLES.createCapabilities(); // -> GLESCapabilities
		glClearColor(0.0f, 0.5f, 0.5f, 0.0f);
		glEnable(GL_CULL_FACE);
		glCullFace(GL_BACK);

		try (final MemoryStack stack = MemoryStack.stackPush()) {
			final IntBuffer ok = stack.callocInt(1);
			final int vs = glCreateShader(GL_VERTEX_SHADER);
			glShaderSource(vs, """
					#version 300 es
					precision mediump float;
					uniform mat4 uMVP;
					uniform vec4 uColor;
					layout (location = 0) in vec4 vPos;
					layout (location = 1) in vec4 vColor;
					layout (location = 2) in vec2 vTex;
					out vec4 fColor;
					out vec2 fTex;
					void main() {
					    gl_Position = uMVP * vPos;
					    fColor = uColor * (vColor + 0.5);
					    fTex = vTex;
					}""");
			glCompileShader(vs);
			glGetShaderiv(vs, GL_COMPILE_STATUS, ok);
			if (ok.get(0) == 0)
				throw new AssertionError("glCompileShader(VS) failed: " + glGetShaderInfoLog(vs));

			final int fs = glCreateShader(GL_FRAGMENT_SHADER);
			glShaderSource(fs, """
					#version 300 es
					precision mediump float;
					uniform sampler2D uTex0;
					uniform highp float uTime;
					in mediump vec4 fColor;
					in vec2 fTex;
					out lowp vec4 outColor;
					void main() {
					    outColor = texture(uTex0, fTex) * (fColor + uTime);
					}""");
			glCompileShader(fs);
			glGetShaderiv(fs, GL_COMPILE_STATUS, ok);
			if (ok.get(0) == 0)
				throw new AssertionError("glCompileShader(FS) failed: " + glGetShaderInfoLog(fs));

			program = glCreateProgram();
			glAttachShader(program, vs);
			glAttachShader(program, fs);
			glLinkProgram(program);
			glGetProgramiv(program, GL_LINK_STATUS, ok);
			if (ok.get(0) == 0)
				throw new AssertionError("glLinkProgram failed: " + glGetProgramInfoLog(program));
			glDeleteShader(vs);
			glDeleteShader(fs);
			if ((mvpLoc = glGetUniformLocation(program, "uMVP")) == -1)
				throw new AssertionError("glGetUniformLocation(uMVP) failed");
			if ((colorLoc = glGetUniformLocation(program, "uColor")) == -1)
				throw new AssertionError("glGetUniformLocation(uColor) failed");
			if ((tex0Loc = glGetUniformLocation(program, "uTex0")) == -1)
				throw new AssertionError("glGetUniformLocation(uTex0) failed");
			if ((timeLoc = glGetUniformLocation(program, "uTime")) == -1)
				throw new AssertionError("glGetUniformLocation(uTime) failed");

			tex0 = glGenTextures();
			glBindTexture(GL_TEXTURE_2D, tex0);
			final ByteBuffer texBuf = stack.malloc(4, 2 * 2 * 4);
			texBuf.put(0, new byte[]{ // R,G,B,A
					-1, -1, -1, -1, 127, 127, 127, -1,
					127, 127, 127, -1, -1, -1, -1, -1,
			});
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, texBuf);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

			final IntBuffer bo2 = stack.callocInt(2);
			glGenBuffers(bo2);
			final int vbo = bo2.get(0);
			final int ebo = bo2.get(1);
			glBindBuffer(GL_ARRAY_BUFFER, vbo);
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
			glBufferData(GL_ARRAY_BUFFER, new float[]{
					//x/r,   y/g,  z/b, a, s, t,
					-0.5f, -0.5f, 0.0f, 1, 0, 1, // bottom left
					+0.5f, -0.5f, 0.0f, 1, 1, 1, // bottom right
					-0.5f, +0.5f, 0.0f, 1, 0, 0, // top left
					+0.5f, +0.5f, 0.0f, 1, 1, 0, // top right
			}, GL_STATIC_DRAW);
			glBufferData(GL_ELEMENT_ARRAY_BUFFER, new short[]{
					0, 1, 2, // triangle-0
					2, 1, 3, // triangle-1
			}, GL_STATIC_DRAW);

			vao = GLES30.glGenVertexArrays();
			GLES30.glBindVertexArray(vao);
			glBindBuffer(GL_ARRAY_BUFFER, vbo);
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
			glVertexAttribPointer(/*layoutLocation:*/0, /*valueType.size:*/3, /*valueType:*/GL_FLOAT,
					/*normalized:*/false, /*stride:*/6 * 4, /*offset:*/0);
			glVertexAttribPointer(/*layoutLocation:*/1, /*valueType.size:*/4, /*valueType:*/GL_FLOAT,
					/*normalized:*/false, /*stride:*/6 * 4, /*offset:*/0);
			glVertexAttribPointer(/*layoutLocation:*/2, /*valueType.size:*/2, /*valueType:*/GL_FLOAT,
					/*normalized:*/false, /*stride:*/6 * 4, /*offset:*/4 * 4);
			glEnableVertexAttribArray(/*layoutLocation:*/0);
			glEnableVertexAttribArray(/*layoutLocation:*/1);
			glEnableVertexAttribArray(/*layoutLocation:*/2);
			GLES30.glBindVertexArray(0);

			glDeleteBuffers(bo2);
		}
	}

	private static void exit() {
		if (vao != 0) {
			GLES30.glDeleteVertexArrays(vao);
			vao = 0;
		}
		if (tex0 != 0) {
			glDeleteTextures(tex0);
			tex0 = 0;
		}
		timeLoc = 0;
		tex0Loc = 0;
		colorLoc = 0;
		mvpLoc = 0;
		if (program != 0) {
			glDeleteProgram(program);
			program = 0;
		}
		if (window != 0) {
			Callbacks.glfwFreeCallbacks(window);
			glfwDestroyWindow(window);
			window = 0;
		}
		glfwTerminate();
		try (final GLFWErrorCallback _ = glfwSetErrorCallback(null)) {
		}
	}

	private static void loop() {
		final FloatBuffer mat = BufferUtils.createFloatBuffer(4 * 4);
		mat.put(0, new float[]{
				1, 0, 0, 0,
				0, 1, 0, 0,
				0, 0, 1, 0,
				0, 0, 0, 1,
		});
		while (!glfwWindowShouldClose(window)) {
			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

			glUseProgram(program);
			glUniformMatrix4fv(mvpLoc, false, mat);
			glUniform4f(colorLoc, 1, 1, 1, 1);
			glUniform1i(tex0Loc, 0);
			glUniform1f(timeLoc, Math.abs(System.currentTimeMillis() % 2000 * 0.0005f - 0.5f) - 0.25f);
			glActiveTexture(GL_TEXTURE0);
			glBindTexture(GL_TEXTURE_2D, tex0);
			GLES30.glBindVertexArray(vao);
			glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);

			glfwSwapBuffers(window);
			glfwPollEvents();
		}
	}

	public static void main(@SuppressWarnings("unused") String[] args) {
		try {
			init();
			loop();
		} finally {
			exit();
		}
	}
}
